package be.ac.ulb.mlg.utils;

/*
 * The MIT License (MIT)
 * 
 * Copyright (c) 2013 Jean-Sebastien Lerat (Jean-Sebastien.Lerat@ulb.ac.be)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * 
 * @author Jean-Sebastien Lerat (Jean-Sebastien.Lerat@ulb.ac.be)
 * @version 1.00, 24/06/2013
 */

import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import org.junit.Before;
import org.junit.Test;

import be.ac.ulb.mlg.utils.TestUtils.DataSize;

import be.ac.ulb.mlg.utils.measure.Euclidean;
import be.ac.ulb.mlg.utils.measure.Pearson;
import be.ac.ulb.mlg.utils.measure.Spearman;

public class MeasurerTest {
	static final class FakeMeasure implements Measure{
		@Override
		public double measure(double[] a, double[] b, boolean[] computable) {return a[0];}
		@Override
		public boolean hasNativeImplementation() {return false;}
		@Override
		public boolean requireDataTransformation() {return false;}
		@Override
		public void transform(double[][] data) {}
	};
	
	private static final boolean WITHOUT_NORMALIZATION= false;
	private static final boolean WITH_NORMALIZATION	= true;
	private static final boolean WITH_IGNORE		= true;
	private static final boolean WITHOUT_IGNORE		= false;
	private static final boolean WITH_THREADING 	= true;
	private static final boolean WITHOUT_THREADING	= false;
	private static final int THREADING 		= 2;
	private static final int NO_THREADING		= 1;
	private static final DataSize DATA_TESTER		= DataSize.SMALL;
	private static final int WITHOUT_MISSING_VALUE	= 0;
	private static final int WITH_MISSING_VALUE		= Math.min(DATA_TESTER.columns/10,1);
	private static final Measure DEFAULT_MEASURE	= new Euclidean();
	
	private final DataSize size;
	private final double[][] initialData;
	private final boolean[][] initialIgnore;
	private final Measure[] measures;
	
	private double[][] currentData;
	private boolean[][] currentIgnore;
	private Map<Measure,double[][]> result;
	private boolean nativeMode;
	
	public MeasurerTest(){
		this.size			= DATA_TESTER;
		this.initialData	= TestUtils.generateData(this.size,1e2,TestUtils.DEFAULT_P_IGNORE);
		this.initialIgnore	= TestUtils.generateIgnore(this.size);
		this.result			= new HashMap<Measure,double[][]>();
		this.measures		= new Measure[]{DEFAULT_MEASURE};
		
		this.result			= null;
		this.currentData	= null;
		this.currentIgnore	= null;
		this.nativeMode		= false;
	}
	
	@Before
	public void setUp() throws Exception {
		this.result			= null;
		this.nativeMode		= false;
		this.currentData	= TestUtils.copy(this.initialData);
		this.currentIgnore	= TestUtils.copy(this.initialIgnore);
	}

	@Test
	public void testMultipleMeasures() throws Exception{
		final Measure[] measures = {new Euclidean(),new Spearman(),new Pearson()};
		final Map<Measure,double[][]> expected = new HashMap<Measure,double[][]>(measures.length);
		
		final boolean nativeMode = this.nativeMode;
		for(Measure measure:measures){
			setUp();
			testHelper(new Measure[]{measure},WITHOUT_NORMALIZATION,WITHOUT_THREADING,WITHOUT_IGNORE,WITHOUT_MISSING_VALUE,false);
			expected.putAll(this.result);
		}
		
		setUp();
		this.nativeMode = nativeMode;
		testHelper(measures,WITHOUT_NORMALIZATION,WITHOUT_THREADING,WITHOUT_IGNORE,WITHOUT_MISSING_VALUE,this.nativeMode);
		final Map<Measure,double[][]> actual = this.result;
		
		dataPreserved();
		checkConsistency(measures);
		
		equivalent("MultipleMeasure",expected,actual);
	}

	@Test
	public void testMissing(){
		final int N = this.initialData.length;
		testHelper(WITHOUT_NORMALIZATION,WITHOUT_THREADING,WITHOUT_IGNORE,WITH_MISSING_VALUE);
		boolean allIgnorated = true;
		final double[][] result = this.result.values().iterator().next();
		for(int i=0;i<N-1;i++){
			boolean isMissing = isMissing(this.initialData[i],WITH_MISSING_VALUE);
			for(int j=i+1,k=0;j<N;j++,k++){
				isMissing |= isMissing(this.initialData[j],WITH_MISSING_VALUE);
				if(isMissing && !isMissing(result[i][k])){
					allIgnorated = false;
				}
			}
		}
		assertTrue("Don't ignore all data which must be ignorated", allIgnorated);
	}
	@Test
	public void testIgnore(){
		final int N = this.initialIgnore.length;
		testHelper(WITHOUT_NORMALIZATION,WITHOUT_THREADING,WITH_IGNORE,WITHOUT_MISSING_VALUE);
		boolean allIgnorated = true;
		final double[][] result = this.result.values().iterator().next();
		for(int i=0;i<N-1;i++)
			for(int j=i+1,k=0;j<N;j++,k++)
				if(this.initialIgnore[i][j]==TestUtils.IGNORE && !isMissing(result[i][k])){
					allIgnorated = false;
				}
		assertTrue("Don't ignore all data which must be ignorated", allIgnorated);
	}

	@Test
	public void testBasic(){
		testHelper(WITHOUT_NORMALIZATION,WITHOUT_THREADING,WITHOUT_IGNORE,WITHOUT_MISSING_VALUE);
		dataPreserved();
		checkConsistency();
	}

	@Test
	public void testThreads(){
		testHelper(WITHOUT_NORMALIZATION,WITHOUT_THREADING,WITHOUT_IGNORE,WITHOUT_MISSING_VALUE);
		final Map<Measure,double[][]> backup	= this.result;
		final Measure key						= backup.keySet().iterator().next();
		
		testHelper(WITHOUT_NORMALIZATION,WITH_THREADING,WITHOUT_IGNORE,WITHOUT_MISSING_VALUE);
		dataPreserved();
		checkConsistency();
		
		assertTrue("Input data has changed",TestUtils.equals(backup.get(key),this.result.get(key)));
	}
	
	@Test
	public void testNormalize(){
		testHelper(WITH_NORMALIZATION,WITHOUT_THREADING,WITHOUT_IGNORE,WITHOUT_MISSING_VALUE);
		checkConsistency();
	}

	@Test
	public void testShuffle(){
		final int RUNS		= 1000;
		final double pError = 0.1;
		
		//assuming the same behavior that in the basic run, we can iterate a lot of time to check the probability of shuffling
		final Measurer measurer = new Measurer(this.nativeMode,MeasurerTest.WITHOUT_MISSING_VALUE,Measurer.NO_RENORMALIZER);
		measurer.setNumberOfThreads(NO_THREADING);
		measurer.setHandelingMissingValues(false);
		
		final double[][] data	= {{0.0},{1.0}};
		final Measure[] fake		= {new FakeMeasure()};
		final int[] counter	= {0,0};
		
		//without shuffle
		measurer.useShuffling(false);
		for(int iter=0;iter<RUNS;iter++){
			this.result = measurer.measure(data,fake,null);
			final int value = (int) (this.result.values().iterator().next()[0][0]);
			counter[value]++;
		}
		assertEquals("Without shuffle cannot shuffle the order of input vectors",RUNS, counter[0]);
		
		//with shuffle
		Arrays.fill(counter,0);
		measurer.useShuffling(true);
		for(int iter=0;iter<RUNS;iter++){
			this.result = measurer.measure(data,fake,null);
			final int value = (int) (this.result.values().iterator().next()[0][0]);
			counter[value]++;
		}
		final double pShuffle = 1.0-(((double)Math.abs(counter[0]-counter[1]))/RUNS);
		assertTrue("With shuffle must shuffle the order of input vectors with a high probability",pShuffle>(1.0-pError));
	}
	@Test
	public void testNative() throws Exception{
		Map<Measure,double[][]> backup;
		//test basic measurement
		{
			setUp();
			testBasic();
			backup = this.result;
			setUp();
			this.nativeMode = true;
			testBasic();
			equivalent("Native(Basic)",backup,this.result);
		}
		//test ignore measurement
		{
			setUp();
			testIgnore();
			backup = this.result;
			setUp();
			this.nativeMode = true;
			testIgnore();
			equivalent("Native(Ignore)",backup,this.result);
		}
		//test missing measurement
		{
			setUp();
			testMissing();
			backup = this.result;
			setUp();
			this.nativeMode = true;
			testMissing();
			equivalent("Native(Missing)",backup,this.result);
		}
		//test normalization measurement
		{
			setUp();
			testNormalize();
			backup = this.result;
			setUp();
			this.nativeMode = true;
			testNormalize();
			equivalent("Native(Normalize)",backup,this.result);
		}
		//test multiple measures measurement
		{
			setUp();
			testMultipleMeasures();
			backup = this.result;
			setUp();
			this.nativeMode = true;
			testMultipleMeasures();
			equivalent("Native(MultipleMeasures)",backup,this.result);
		}
		//test threading measurement
		{
			setUp();
			testThreads();
			backup = this.result;
			setUp();
			this.nativeMode = true;
			testThreads();
			equivalent("Native(Threads)",backup,this.result);
		}
	}
	
	
	
	private static boolean isMissing(final double[] vect,final int missing) {
		int count = 0;
		for(double v:vect)
			if(isMissing(v))
				count++;
		return count > missing ;
	}
	private static boolean isMissing(double val) {
		return Double.isNaN(val);
	}
	private void equivalent(String message, Map<Measure, double[][]> expected, Map<Measure, double[][]> actual) {
		final Map<String,double[][]> strExpected = new HashMap<String,double[][]>(expected.size());
		final Map<String,double[][]> strActual = new HashMap<String,double[][]>(actual.size());
		for(Measure measure:expected.keySet())
			strExpected.put(measure.getClass().getName(),expected.get(measure));
		for(Measure measure:actual.keySet())
			strActual.put(measure.getClass().getName(),actual.get(measure));
		equivalent(strExpected, strActual,message);
	}
	private void equivalent(Map<String, double[][]> expected, Map<String, double[][]> actual,String message) {
		assertEquals(message+"- Number of results differs",expected.size(),actual.size());
		for(String measure:expected.keySet()){
			if(!actual.containsKey(measure))
				fail(message+"- All measures are not processed["+measure+"]");
			else{
				final double[][] a = expected.get(measure);
				final double[][] b = actual.get(measure);
				if(!TestUtils.equals(expected.get(measure),actual.get(measure))){
					assertTrue(message+" - Native code ["+measure+"] gives different results than Java",TestUtils.equals(a,b));
				}
			}
		}
	}

	private void dataPreserved(){
		assertTrue("Input data has changed",TestUtils.equals(this.initialData,this.currentData));
	}

	private void checkConsistency(final double[][] result) {
		assertEquals("Bad number of comparisons", this.initialData.length-1,result.length);
		int count = result.length;
		for(double[] sub:result)
			assertEquals("Bad number of values",count--, sub.length);
	}
	private void checkConsistency() {
		checkConsistency(new Measure[]{DEFAULT_MEASURE});
	}
	private void checkConsistency(Measure[] givenMeasures) {
		final Vector<Measure> processed = new Vector<Measure>(this.result.keySet());
		for(Measure measure:givenMeasures){
			if(!processed.remove(measure))
				fail("All measures are not processed");
		}
		assertEquals("All measures are not processed correclty",0,processed.size());
		
		for(Measure measure:this.result.keySet())
			checkConsistency(this.result.get(measure));
	}
	private void testHelper(final boolean normalize,final boolean useThread,final boolean ignore,final int missingValues){
		testHelper(this.measures,normalize,useThread,ignore,missingValues,this.nativeMode);
	}
	private void testHelper(final Measure[] measures,final boolean normalize,final boolean useThread,final boolean ignore,final int missingValues,final boolean useNative){
		if(useNative && (!  Measurer.hasNativeSupport()))
			fail("Native support are not available");
		
		final Renormalizer renorm = normalize? new DefaultRenormalizer():Measurer.NO_RENORMALIZER;
		final boolean[][] ignored = ignore?this.currentIgnore:null;
		
		final Measurer measurer = new Measurer(useNative,missingValues,renorm);
		measurer.setNumberOfThreads(useThread?THREADING:NO_THREADING);
		measurer.setHandelingMissingValues(missingValues>0);
		this.result = measurer.measure(this.currentData,measures,ignored);
	}
}
